package internal

import (
	"encoding/hex"
	"fmt"
	"github.com/cavaliercoder/go-rpm"
	"github.com/go-git/go-billy/v5/memfs"
	"github.com/go-git/go-git/v5"
	"github.com/go-git/go-git/v5/config"
	"github.com/go-git/go-git/v5/plumbing"
	"github.com/go-git/go-git/v5/plumbing/object"
	"github.com/go-git/go-git/v5/plumbing/transport/ssh"
	"github.com/go-git/go-git/v5/storage/memory"
	"github.com/mstg/srpmproc/internal/blob"
	"hash"
	"io/ioutil"
	"log"
	"path/filepath"
	"regexp"
	"strings"
	"time"
)

var tagImportRegex = regexp.MustCompile("refs/tags/(imports/(.*)/(.*))")

type ProcessData struct {
	RpmLocation       string
	UpstreamPrefix    string
	SshKeyLocation    string
	SshUser           string
	Version           int
	GitCommitterName  string
	GitCommitterEmail string
	Mode              int
	ModulePrefix      string
	Authenticator     *ssh.PublicKeys
	Importer          ImportMode
	BlobStorage       blob.Storage
	NoDupMode         bool
	ModuleMode        bool
}

type ignoredSource struct {
	name         string
	hashFunction hash.Hash
	expired      bool
}

type modeData struct {
	repo            *git.Repository
	worktree        *git.Worktree
	rpmFile         *rpm.PackageFile
	fileWrites      map[string][]byte
	tagBranch       string
	pushBranch      string
	branches        []string
	sourcesToIgnore []*ignoredSource
}

// ProcessRPM checks the RPM specs and discards any remote files
// This functions also sorts files into directories
// .spec files goes into -> SPECS
// metadata files goes to root
// source files goes into -> SOURCES
// all files that are remote goes into .gitignore
// all ignored files' hash goes into .{name}.metadata
func ProcessRPM(pd *ProcessData) {
	md := pd.Importer.RetrieveSource(pd)

	remotePrefix := "dist"
	if pd.ModuleMode {
		remotePrefix = "modules"
	}

	// if no-dup-mode is enabled then skip already imported versions
	var tagIgnoreList []string
	if pd.NoDupMode {
		repo, err := git.Init(memory.NewStorage(), memfs.New())
		if err != nil {
			log.Fatalf("could not init git repo: %v", err)
		}
		remoteUrl := fmt.Sprintf("%s/%s/%s.git", pd.UpstreamPrefix, remotePrefix, md.rpmFile.Name())
		refspec := config.RefSpec("+refs/heads/*:refs/remotes/origin/*")

		remote, err := repo.CreateRemote(&config.RemoteConfig{
			Name:  "origin",
			URLs:  []string{remoteUrl},
			Fetch: []config.RefSpec{refspec},
		})
		if err != nil {
			log.Fatalf("could not create remote: %v", err)
		}

		list, err := remote.List(&git.ListOptions{
			Auth: pd.Authenticator,
		})
		if err != nil {
			log.Println("ignoring no-dup-mode")
		} else {
			for _, ref := range list {
				if !strings.HasPrefix(string(ref.Name()), "refs/tags/imports") {
					continue
				}
				tagIgnoreList = append(tagIgnoreList, string(ref.Name()))
			}
		}
	}

	sourceRepo := *md.repo
	sourceWorktree := *md.worktree

	for _, branch := range md.branches {
		md.repo = &sourceRepo
		md.worktree = &sourceWorktree
		md.tagBranch = branch
		for _, source := range md.sourcesToIgnore {
			source.expired = true
		}

		rpmFile := md.rpmFile
		// create new repo for final dist
		repo, err := git.Init(memory.NewStorage(), memfs.New())
		if err != nil {
			log.Fatalf("could not create new dist repo: %v", err)
		}
		w, err := repo.Worktree()
		if err != nil {
			log.Fatalf("could not get dist worktree: %v", err)
		}

		var matchString string
		if !tagImportRegex.MatchString(md.tagBranch) {
			if pd.ModuleMode {
				prefix := fmt.Sprintf("refs/heads/c%d", pd.Version)
				if strings.HasPrefix(md.tagBranch, prefix) {
					replace := strings.Replace(md.tagBranch, "refs/heads/", "", 1)
					matchString = fmt.Sprintf("refs/tags/imports/%s/%s", replace, filepath.Base(pd.RpmLocation))
					log.Printf("using match string: %s", matchString)
				}
			}
			if !tagImportRegex.MatchString(matchString) {
				log.Fatal("import tag invalid")
			}
		} else {
			matchString = md.tagBranch
		}

		match := tagImportRegex.FindStringSubmatch(matchString)
		md.pushBranch = "rocky" + strings.TrimPrefix(match[2], "c")
		newTag := "imports/rocky" + strings.TrimPrefix(match[1], "imports/c")

		shouldContinue := true
		for _, ignoredTag := range tagIgnoreList {
			if ignoredTag == "refs/tags/"+newTag {
				log.Printf("skipping %s", ignoredTag)
				shouldContinue = false
			}
		}
		if !shouldContinue {
			continue
		}

		// create a new remote
		remoteUrl := fmt.Sprintf("%s/%s/%s.git", pd.UpstreamPrefix, remotePrefix, rpmFile.Name())
		log.Printf("using remote: %s", remoteUrl)
		refspec := config.RefSpec(fmt.Sprintf("+refs/heads/%s:refs/remotes/origin/%s", md.pushBranch, md.pushBranch))
		log.Printf("using refspec: %s", refspec)

		_, err = repo.CreateRemote(&config.RemoteConfig{
			Name:  "origin",
			URLs:  []string{remoteUrl},
			Fetch: []config.RefSpec{refspec},
		})
		if err != nil {
			log.Fatalf("could not create remote: %v", err)
		}

		err = repo.Fetch(&git.FetchOptions{
			RemoteName: "origin",
			RefSpecs:   []config.RefSpec{refspec},
			Auth:       pd.Authenticator,
		})

		refName := plumbing.NewBranchReferenceName(md.pushBranch)
		log.Printf("set reference to ref: %s", refName)

		if err != nil {
			h := plumbing.NewSymbolicReference(plumbing.HEAD, refName)
			if err := repo.Storer.CheckAndSetReference(h, nil); err != nil {
				log.Fatalf("could not set reference: %v", err)
			}
		} else {
			err = w.Checkout(&git.CheckoutOptions{
				Branch: plumbing.NewRemoteReferenceName("origin", md.pushBranch),
				Force:  true,
			})
			if err != nil {
				log.Fatalf("could not checkout: %v", err)
			}
		}

		pd.Importer.WriteSource(md)

		copyFromFs(md.worktree.Filesystem, w.Filesystem, ".")
		md.repo = repo
		md.worktree = w

		if pd.ModuleMode {
			patchModuleYaml(pd, md)
		} else {
			executePatchesRpm(pd, md)
		}

		// already uploaded blobs are skipped
		var alreadyUploadedBlobs []string
		// get ignored files hash and add to .{name}.metadata
		metadataFile := fmt.Sprintf(".%s.metadata", rpmFile.Name())
		metadata, err := w.Filesystem.Create(metadataFile)
		if err != nil {
			log.Fatalf("could not create metadata file: %v", err)
		}
		for _, source := range md.sourcesToIgnore {
			if source.expired {
				continue
			}

			sourcePath := source.name
			sourceFile, err := w.Filesystem.Open(sourcePath)
			if err != nil {
				log.Fatalf("could not open ignored source file %s: %v", sourcePath, err)
			}
			sourceFileBts, err := ioutil.ReadAll(sourceFile)
			if err != nil {
				log.Fatalf("could not read the whole of ignored source file: %v", err)
			}

			source.hashFunction.Reset()
			_, err = source.hashFunction.Write(sourceFileBts)
			if err != nil {
				log.Fatalf("could not write bytes to hash function: %v", err)
			}
			checksum := hex.EncodeToString(source.hashFunction.Sum(nil))
			checksumLine := fmt.Sprintf("%s %s\n", checksum, sourcePath)
			_, err = metadata.Write([]byte(checksumLine))
			if err != nil {
				log.Fatalf("could not write to metadata file: %v", err)
			}

			path := fmt.Sprintf("%s", checksum)
			if strContains(alreadyUploadedBlobs, path) {
				continue
			}
			pd.BlobStorage.Write(path, sourceFileBts)
			log.Printf("wrote %s to blob storage", path)
			alreadyUploadedBlobs = append(alreadyUploadedBlobs, path)
		}

		_, err = w.Add(metadataFile)
		if err != nil {
			log.Fatalf("could not add metadata file: %v", err)
		}

		lastFilesToAdd := []string{".gitignore", "SPECS"}
		for _, f := range lastFilesToAdd {
			_, err := w.Filesystem.Stat(f)
			if err == nil {
				_, err := w.Add(f)
				if err != nil {
					log.Fatalf("could not add %s: %v", f, err)
				}
			}
		}

		pd.Importer.PostProcess(md)

		// show status
		status, _ := w.Status()
		log.Printf("successfully processed:\n%s", status)

		var hashes []plumbing.Hash
		var pushRefspecs []config.RefSpec

		head, err := repo.Head()
		if err != nil {
			hashes = nil
			pushRefspecs = append(pushRefspecs, "*:*")
		} else {
			log.Printf("tip %s", head.String())
			hashes = append(hashes, head.Hash())
			refOrigin := "refs/heads/" + md.pushBranch
			pushRefspecs = append(pushRefspecs, config.RefSpec(fmt.Sprintf("HEAD:%s", refOrigin)))
		}

		// we are now finished with the tree and are going to push it to the src repo
		// create import commit
		commit, err := w.Commit("import "+pd.Importer.ImportName(pd, md), &git.CommitOptions{
			Author: &object.Signature{
				Name:  pd.GitCommitterName,
				Email: pd.GitCommitterEmail,
				When:  time.Now(),
			},
			Parents: hashes,
		})
		if err != nil {
			log.Fatalf("could not commit object: %v", err)
		}

		obj, err := repo.CommitObject(commit)
		if err != nil {
			log.Fatalf("could not get commit object: %v", err)
		}

		log.Printf("committed:\n%s", obj.String())

		_, err = repo.CreateTag(newTag, commit, &git.CreateTagOptions{
			Tagger: &object.Signature{
				Name:  pd.GitCommitterName,
				Email: pd.GitCommitterEmail,
				When:  time.Now(),
			},
			Message: "import " + md.tagBranch + " from " + pd.RpmLocation,
			SignKey: nil,
		})
		if err != nil {
			log.Fatalf("could not create tag: %v", err)
		}

		pushRefspecs = append(pushRefspecs, config.RefSpec(fmt.Sprintf("HEAD:%s", plumbing.NewTagReferenceName(newTag))))

		err = repo.Push(&git.PushOptions{
			RemoteName: "origin",
			Auth:       pd.Authenticator,
			RefSpecs:   pushRefspecs,
			Force:      true,
		})
		if err != nil {
			log.Fatalf("could not push to remote: %v", err)
		}
	}
}
